/*
 * Funambol is a mobile platform developed by Funambol, Inc. 
 * Copyright (C) 2003 - 2007 Funambol, Inc.
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission 
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE 
 * WARRANTY OF NON INFRINGEMENT  OF THIRD PARTY RIGHTS.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License 
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 * 
 * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite 
 * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
 * 
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 * 
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "Powered by Funambol" logo. If the display of the logo is not reasonably 
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by Funambol".
 */
#include "EventDataConverter.h"
#include "base/util/StringBuffer.h"
#include "vocl/VProperty.h"
#include "vocl/VObject.h"
#include "vocl/VConverter.h"
#include "Event.h"
#include "base/Log.h"

StringBuffer formatBias(const long bias) {
    
    char tmp[6];
    
    int hours   = bias / 60;
    int minutes = bias % 60;
    if (bias >= 0) { sprintf(tmp, "-%02d%02d", hours,   minutes); }
    else           { sprintf(tmp, "+%02d%02d", -hours, -minutes); }
    
    StringBuffer out(tmp);
    return out;
}


int parseBias(const char* data) {
    
    int bias    = 0;
    int hours   = 0;
    int minutes = 0;
    int len = strlen(data);
    
    if (data && len>1) {
        int sign  = 1;              // The sign of the bias (default "+1" if not found).
        int start = 1;              // Starting position of the first digit.
        
        if (isdigit(data[0])) { 
            start = 0;
            sign = 1;
        }
        else if (data[0] == '-') {
            start = 1;
            sign = -1;
        }
        
        if (len-start < 3) {
            // "+08"
            sscanf(&data[start], "%2d", &hours);
        }
        else {
            // "+0800"
            sscanf(&data[start], "%2d%2d", &hours, &minutes);
        }
        
        bias = ((hours*60) + minutes) * (-sign);    // e.g. "+0130" -> -90
    }
    
    return bias;
}



/**
 * Returns true if startdate = 00:00 and enddate = 23:59.
 * This is an all-day-event.
 * @ tobe implemented
 */
bool isAllDayInterval(const char*  startdate, const char*  enddate) {
   
    bool ret = false;    
    return ret;
}

Date convertCalPropertyToDate(const char* element, Timezone& tz) { 
    Date d(element);    
    d.setTimezone(tz);
    return d;
}

StringBuffer convertDateToCalProperty(Date& d) {    
    return d.getDate();   
}

char* EventDataConverter::getVObjectPropertyValue(VObject* vo, const char* propertyName) {

    char* propertyValue = NULL;
    VProperty* vprop = vo->getProperty(propertyName);
    if (vprop && vprop->getValue()) {
        propertyValue = vprop->getValue();
    }
    return propertyValue;
}


PIMItem* EventDataConverter::parse(const char* stringEvent) {
    
    Event* event = NULL;
    Alarm* alarm = NULL;
    char* element = NULL;
    
    StringBuffer startDateValue;
    StringBuffer endDateValue;
    
    VObject* vo = VConverter::parse(stringEvent);
    if (!vo) {        
        LOG.error("Error in EventDataConverter parse");
        return event;
    }       
        
    event = new Event();
    
    Timezone tz;
    // check if there is the timezone and set in the date object
    StringBuffer timezone = parseTimezone(vo);
    if (timezone == "") {
        timezone = getCurrentTimezone();
    }
    tz.setName(timezone);
    event->setEventTimezone(tz);
    
    // 
    // DTSTART, DTEND:
    // Set the start and end date. If the start is 00:00 and end is 23:59 the appointment is decided to be
    // an all day event. So the AllDayEvent property is set to '1' in the propertyMap.
    //
    if(element = getVObjectPropertyValue(vo, "DTSTART")){
        LOG.debug("the element in the parser is %s", element);        
        startDateValue = element;
        Date start = convertCalPropertyToDate(element, tz);  
        LOG.debug("the date in the parser is %s", start.getDate());
        event->setStart(start);        
    }
    if(element = getVObjectPropertyValue(vo, "DTEND")) {
        endDateValue = element;
        Date end = convertCalPropertyToDate(element, tz);        
        event->setEnd(end);                
    }

    if (startDateValue.length() > 0 && endDateValue.length() > 0) {
        // ALL-DAY EVENT
        bool isAllDay = false;
        if(element = getVObjectPropertyValue(vo, "X-FUNAMBOL-ALLDAY")){
            isAllDay = strcmp(element, "1")?  false : true;
        }      
        if (!isAllDay) {
            // All-day check #2: interval [00:00 - 23:59]
            isAllDay = isAllDayInterval(startDateValue, endDateValue);
        }
        if (!isAllDay) {
            // All-day check #3: format "yyyyMMdd"
            if (startDateValue.length() == 8 && endDateValue.length() == 8 ) {
                isAllDay = true;
            }
        }
        event->setAllDayEvent(isAllDay);               
    }        
    
    if(element = getVObjectPropertyValue(vo, "SUMMARY")) {        
        event->setSubject(element);
    }
    if(element = getVObjectPropertyValue(vo, "LOCATION")) {
        event->setLocation(element);
    }    
    if(element = getVObjectPropertyValue(vo, "DESCRIPTION")) {
        event->setBody(element);
    }                     
    if(element = getVObjectPropertyValue(vo, "CATEGORIES")) {
        event->setCategories(element);
    }
    
    if(element = getVObjectPropertyValue(vo, "PRIORITY")) {        
        Importance imp = ImportanceNormal;
        if (!strcmp(element, "0") ) {
            imp =  ImportanceLow; 
        }
        else if (!strcmp(element, "2") ) {
            imp = ImportanceHigh;
        }                
        event->setImportance(imp);
    }
    if (element = getVObjectPropertyValue(vo, "CLASS")) {
        Sensitivity s = NormalEvt;
        if (!strcmp(element, "CONFIDENTIAL") ) {
            s = ConfidentialEvt; 
        }
        else if (!strcmp(element, "PRIVATE") ) {
            s = PrivateEvt;
        }
        event->setSensitivity(s);
    }
    if(element = getVObjectPropertyValue(vo, "X-MICROSOFT-CDO-BUSYSTATUS")) {
        BusyStatus busy = Free;
        if (!strcmp(element, "1") ) {
            busy = Tentative; 
        }
        else if (!strcmp(element, "2") ) {
            busy = Busy;
        } 
        else if (!strcmp(element, "3") ) {
            busy = OutOfOffice;
        }                
        event->setBusyStatus(busy);        
    }
    
    if ( (element = getVObjectPropertyValue(vo, "RRULE")) && 
         (strlen(element) > 0) ) {
        
        event->setIsRecurring(true);    
        RecurrenceDataConverter rdc;
        Recurrence* rec = rdc.parse(element, event->getStart());
        
        if (rec) {
            event->setRecurrence(rec); 
        } else {
            LOG.error("EventDataConverter::parse - Error creating the recurrence object. It is not added to the event");
        }    
        
        // @todo: handle the exception dates
        /*
        StringBuffer timezone = parseTimezone(vo);
        if (timezone != "") {
            Timezone tz;
            tz.setName(timezone);
            event->setEventTimezone(tz);
        }
        */ 
        
    }
    else {
        // Not recurring.
        event->setIsRecurring(false);
    }
    
    if(element = getVObjectPropertyValue(vo, "AALARM")) {
        
        char* runTimeValue = vo->getProperty("AALARM")->getPropComponent(1);
        if (strlen(runTimeValue) > 0) {
            event->setReminderSet(true);
            // create a new Alarm object. At the moment the Alarm object can be only one
                        
            Date al = convertCalPropertyToDate(element, tz);  
            LOG.debug("The aalarm date in the parser is %s", al.getDate());
            
            alarm = new Alarm();
            alarm->setPrimaryAlarm(al);            
        }
        else {
            
            event->setReminderSet(false);
        }
    }
    else {
        // AALARM not found -> reset reminder!
        // Note: this is done for compatibility with most devices: if alarm not set
        //       AALARM property is not sent.
        event->setReminderSet(false);
    }
    
    if (event->getReminderSet()) {
        if (alarm) {
            alarm->completeFields(event->getStart());
            event->setAlarm(alarm);
        }
    }
    
    return (PIMItem*)event;
    
}

const char* EventDataConverter::format(PIMItem& item) {
    
    Event& event = (Event&)item;
    
    bool isRecurring = event.getIsRecurring();
    
    VObject* vo = new VObject();
    VProperty* vp  = NULL;
    Date startdate = NULL;
    StringBuffer element;

    vp = new VProperty("BEGIN", "VCALENDAR");
    vo->addProperty(vp);
    delete vp; vp = NULL;

    vp = new VProperty("VERSION", "1.0");
    vo->addProperty(vp);
    delete vp; vp = NULL;
    
    if (isRecurring) {
        // add the timezone information
        addTimezone(vo, event); 
    }
    
    vp = new VProperty("BEGIN", "VEVENT");
    vo->addProperty(vp);
    delete vp; vp = NULL;
    
    // all day       
    vp = new VProperty(TEXT("X-FUNAMBOL-ALLDAY"), event.getAllDayEvent() ? "1" : "0");    
    vo->addProperty(vp);
    delete vp; vp = NULL;
    
    // start
    Date& start = event.getStart();
    StringBuffer date = convertDateToCalProperty(start);
    vp = new VProperty("DTSTART", date);
    vo->addProperty(vp);
    delete vp; vp = NULL;
    
    Date& end = event.getEnd();
    date = convertDateToCalProperty(end);    
    vp = new VProperty("DTEND", date);
    vo->addProperty(vp);
    delete vp; vp = NULL;
    
    BusyStatus busy = event.getBusyStatus();
    StringBuffer busystatus;
    if      (busy == Tentative)     busystatus.append(Tentative);
    else if (busy == Busy)          busystatus.append(Busy);
    else if (busy == OutOfOffice)   busystatus.append(OutOfOffice);
    else                            busystatus.append(Free);
    vp = new VProperty("X-MICROSOFT-CDO-BUSYSTATUS", busystatus);
    vo->addProperty(vp);
    delete vp; vp = NULL;   
    
    element = event.getCategories();
    vp = new VProperty("CATEGORIES", element);
    vo->addProperty(vp);    
    delete vp; vp = NULL;
    element = "";

    element = event.getBody();
    vp = new VProperty("DESCRIPTION", element.c_str());
    vo->addProperty(vp);
    delete vp; vp = NULL;
    element = "";
    
    element = event.getLocation();
    vp = new VProperty("LOCATION", element.c_str());
    vo->addProperty(vp);
    delete vp; vp = NULL;
    element = "";

    Importance imp = event.getImportance();
    StringBuffer importance;
    if      (imp == ImportanceLow)  importance.append(ImportanceLow);
    else if (imp == ImportanceHigh) importance.append(ImportanceHigh);
    else                            importance.append(ImportanceNormal);
    vp = new VProperty("PRIORITY", importance);
    vo->addProperty(vp);
    delete vp; vp = NULL;
    
    element = event.getSubject();
    vp = new VProperty("SUMMARY", element.c_str());
    vo->addProperty(vp);
    delete vp; vp = NULL;
    element = "";
      
    Sensitivity s = event.getSensitivity();
    StringBuffer sensit;
    if      (s == PersonalEvt)      sensit.append("PUBLIC"); // get public value...
    else if (s == PrivateEvt)       sensit.append("PRIVATE");
    else if (s == ConfidentialEvt)  sensit.append("CONFIDENTIAL");
    else                            sensit.append("PUBLIC");                
    vp = new VProperty(TEXT("CLASS"), sensit);
    vo->addProperty(vp);
    delete vp; vp = NULL;
    element = "";
    
    //
    // ReminderSet
    //
    
    if (event.getReminderSet()) {
        Alarm* alarm = event.getAlarm();
        
        vp = new VProperty("AALARM");
        vp->addValue(alarm->getPrimaryAlarm().getDate());   // "RunTime"
        vp->addValue("");                                   // "Snooze Time" (empty)
        vp->addValue("0");                                  // "Repeat Count"
        vp->addValue(element.c_str());                      // "Audio Content" = sound file path
        vo->addProperty(vp);
        delete vp; vp = NULL; 
        
    } else {
        vp = new VProperty("AALARM");
        vo->addProperty(vp);
        delete vp; vp = NULL; 
    }
       
    if (isRecurring) {
        RecurrenceDataConverter rdc;
        const char* rec = rdc.format(*(event.getRecurrence())); 
        if (rec) {
            vp = new VProperty("RRULE", rec);
            LOG.debug("the content of RRULE %s", rec);
            vo->getProperty("RRULE");
            vo->addProperty(vp);
            delete vp; vp = NULL;
        }        
    } else {
        // Not recurring: send empty "RRULE:"
        vp = new VProperty("RRULE");
        vo->addProperty(vp);
        delete vp; vp = NULL;          
    }
    
    vp = new VProperty("END", "VEVENT");
    vo->addProperty(vp);
    delete vp; vp = NULL;

    vp = new VProperty("END", "VCALENDAR");
    vo->addProperty(vp);
    delete vp; vp = NULL;

    vevent = vo->toString();    
    
    return vevent;
    
}



void EventDataConverter::addTimezone(VObject* vo, Event& event) {
    
    VProperty* vp  = NULL;
    
    Recurrence& recurrence = (*(event.getRecurrence()));
    int year = 0, processedYear = 0, recordNumber = 0;
    bool added = false;
    
    ArrayList& days = recurrence.getYearsForTimezoneInfo();
    for (int i = 0; i < days.size(); i++) {
        year = atol(((StringBuffer*)days.get(i))->c_str());
        
        if (year != processedYear && recordNumber <= MAX_RRULE_RECORD_NUMBER) {
            processedYear = year;
            recordNumber++;
            
            StringBuffer standardDate, daylightDate;
            long standardOffset = 0, daylightOffset = 0, onlyOffset = 0;
         
            if (!getTimezoneInfo(year, event.getEventTimezone().getName(), standardDate, daylightDate,
                                 &standardOffset, &daylightOffset, &onlyOffset)) {
                LOG.error("EventDataConverter::addTimezone. It is not possible to calculate the timezone. Empty!!!");
                break;
            } 
            
            if (standardOffset == 0 && daylightOffset == 0) {
            // the daylight is not present...  Use only the offset. This is for timezone that has not day light saving  
                
                vp = new VProperty("TZ", formatBias(onlyOffset).c_str());
                vo->addProperty(vp);
                delete vp; vp = NULL;
                
                vp = new VProperty("DAYLIGHT");
                vp->addValue("FALSE");
                vo->addProperty(vp);
                delete vp; vp = NULL;
                break;
                
            } else {
                if (!added) {
                    vp = new VProperty("TZ", formatBias(standardOffset).c_str());
                    vo->addProperty(vp);
                    delete vp; vp = NULL;
                    added = true;
                }
                vp = new VProperty("DAYLIGHT");
                vp->addValue("TRUE");                               // DST flag
                vp->addValue(formatBias(daylightOffset).c_str());   // DST offset = (Bias + DaylightBias)
                vp->addValue(daylightDate.c_str());                 // Date and time when the DST begins
                vp->addValue(standardDate.c_str());                 // Date and time when the DST ends
                vp->addValue(event.getEventTimezone().getName());   // Standard time designation (optional, could be empty)
                vp->addValue(event.getEventTimezone().getName());   // DST designation (optional, could be empty)
                
                vo->addProperty(vp);
                delete vp; vp = NULL;
                
            }
           
        }
    }
    
}


StringBuffer EventDataConverter::parseTimezone(VObject* vo) {
    
    char* element = NULL;
    StringBuffer ret("");
    
    if ((element = getVObjectPropertyValue(vo, "TZ")) && strlen(element) > 0) {
        
        int bias = parseBias(element);
        
        StringBuffer dstFlag, dstOffset, standardName, daylightName;
       
        //
        // Search all DAYLIGHT properties (one for every year)
        //
        for(int i = 0; i < vo->propertiesCount(); i++) {
            
            VProperty* vp = vo->getProperty(i);
            if (!strcmp(vp->getName(), "DAYLIGHT")) {
                // Found a DAYLIGHT property. Many props are redundant, now are overwritten.
                if (element = vp->getPropComponent(1)) { dstFlag   = element;              }
                if (element = vp->getPropComponent(2)) { dstOffset = element;              }
                if (element = vp->getPropComponent(3)) { /* nothing to do... */ } //daylightDates.push_back(element); }
                if (element = vp->getPropComponent(4)) { /* nothing to do... */ } // standardDates.push_back(element); }
                if (element = vp->getPropComponent(5)) { standardName = element;           }
                if (element = vp->getPropComponent(6)) { daylightName = element;           }
            }
            
            if (standardName != "") {
                if (checkValidTimezone(standardName)) {
                    ret = standardName;
                    break;
                }
            } else if (daylightName != "") {
                if (checkValidTimezone(daylightName)) {
                    ret = daylightName;
                    break;
                } 
            }
        }
        
        if (dstFlag == "FALSE") {
            // Easy timezone, no DST
            StringBuffer tmp("Etc/GMT");
            tmp.append(bias);
            if (checkValidTimezone(tmp.c_str())) {
                ret = tmp;
            }
            
        } else {
            if (ret == "") {
                // Maybe implement another way to retrieve the timezone information
                // from the rules...
                LOG.debug("It is not possible to get the Timezone information...");
            }
        }
        
    }
    else {
         LOG.debug("No timezone received");
    }
    
    return ret;
}

